import type {
  Ref,
} from 'react'
import {
  forwardRef,
  useCallback,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from 'react'
import { subscribeKey } from 'valtio/utils'
import { useSandbox } from './hooks/useSandbox'
import { isValidScript, tryCompile, useStore } from './store'
import { Message } from './Message'

/**
 * This function sorts the given scripts based on their dependencies defined in the import graph.
 * It uses a depth-first search (DFS) algorithm to traverse the import graph.
 * The DFS starts from each script and visits all its dependencies before adding the script to the result.
 * The function modifies the original scripts array to be empty and returns the sorted scripts.
 *
 * @param {string[]} compiledFiles - The scripts to be sorted. This array will be emptied.
 * @param {Record<string, string[]>} importGraph - An object that maps each script to an array of its dependencies.
 * @returns {string[]} The sorted scripts.
 */
function sortScripts(compiledFiles: Record<string, string>, importGraph: Record<string, string[]>): string[] {
  const result: string[] = [] // The sorted scripts
  const visited = new Set() // A set to keep track of the visited scripts during the DFS
  const recursionStack = new Set() // A set to keep track of the nodes currently in the recursion stack

  // The DFS function
  const dfs = (node: string) => {
    if (recursionStack.has(node)) {
      // Circular dependency detected
      throw new Error(`Circular dependency detected: ${node}`)
    }
    if (visited.has(node) || !isValidScript(node))
      return

    visited.add(node)
    recursionStack.add(node)
    importGraph[node]?.forEach(dfs)
    recursionStack.delete(node)
    result.push(compiledFiles[node])
  }

  // Start the DFS from each script
  Object.keys(compiledFiles).forEach(dfs)

  // Return the sorted scripts
  return result
}

function InnerViewer(
  { className, children, style }: { className?: string, children?: any, style: any },
  ref: Ref<{ reset: () => void }>,
) {
  const containerRef = useRef<HTMLDivElement>(null)
  const store = useStore()

  const [errorMsg, setErrorMsg] = useState<string>('')

  const updateViewer = useCallback(() => {
    const result = tryCompile(store)
    if (result) {
      const [compiledFiles, importGraph, css] = result
      const scripts = sortScripts(compiledFiles, importGraph)

      if (css) {
        const scriptToInjectCss
          = `window.__css__ = [];`
          + `setTimeout(()=> {
        document.querySelectorAll('style[css]').forEach(el => el.remove())
        document.head.insertAdjacentHTML('beforeend', \`<style css>${css}</style>\`)
      }, 1)`

        scripts.push(scriptToInjectCss)
      }

      // eslint-disable-next-line ts/no-use-before-define
      proxyRef.current?.eval(scripts)
      setErrorMsg('')
    }
  }, [])

  useEffect(() => {
    subscribeKey(store, 'files', updateViewer)
  }, [])

  useImperativeHandle(ref, () => ({
    reset: updateViewer,
  }))

  const proxyRef = useSandbox(
    containerRef,
    {
      on_error: (event: any) => {
        const msg
          = event.value instanceof Error ? event.value.message : event.value
        if (
          msg.includes('Failed to resolve module specifier')
          || msg.includes('Error resolving module specifier')
        ) {
          setErrorMsg(
            `${msg.replace(/\. Relative references must.*$/, '')
            }.\nTip: edit the "Import Map" tab to specify import paths for dependencies.`,
          )
        }
        else {
          setErrorMsg(msg)
        }
      },
      on_console: (log: any) => {
        if (log.duplicate)
          return

        if (log.level === 'error') {
          if (log.args[0] instanceof Error)
            setErrorMsg(log.args[0].message)
          else
            setErrorMsg(log.args[0])
        }
      },
    },
    updateViewer,
  )

  return (
    <>
      { children || ''}
      <div
        ref={containerRef}
        className={`repl-viewer z-10 relative h-full ${className}`}
        style={style}
      >
        <Message message={errorMsg} onDismiss={() => setErrorMsg('')} />
      </div>
    </>

  )
}

const Viewer = forwardRef(InnerViewer)

export { Viewer }
